package ch.idsia.ai.agents.controllers.myai;

import ch.idsia.ai.agents.Agent;
import ch.idsia.ai.agents.controllers.BasicAIAgent;
import ch.idsia.mario.engine.sprites.Mario;
import ch.idsia.mario.environments.Environment;
import ch.idsia.scenarios.Main;

import java.io.*;
import java.util.Collections;
import java.util.Vector;

/**
 * Created by IntelliJ IDEA.
 * User: Sergey Karakovskiy
 * Date: Apr 8, 2009
 * Time: 4:03:46 AM
 * Package: ch.idsia.controllers.agents.controllers;
 *
 * Mario has a max jump time of 17.
 * levelScene[x][y]. mario is at default position [11][11].
 */
public class MyAI extends BasicAIAgent implements Agent
{
    private boolean LEARN;

    private String outputFile;
    private int trueJumpCounter = 0;
    private Vector surroundings;
    private int score;
    private double learningRate;
    private double LEARNING_MIN = .05;
    private int totalEpisodes;
    private final int MAX_EPISODES = 2000;
    private Ratio[][] learningTable;
    private DecisionTree tree;

    private final int NO_ELEMENT = 0;
    private final int OBSTACLE = 1;
    private final int ENEMY = 2;
    private final String ENEMY_CLASS = "ch.idsia.agents.controllers.Enemy";
    private final String OBSTACLE_CLASS = "ch.idsia.agents.controllers.Obstacle";
    private String previousAction;
    private boolean actionComplete;
    private int currentEnemies;
    private int previousEnemies;
    private int currentThreat;
    private int currentThreatHeight;
    private int previousThreat;
    private int previousMario;
    private int xCell;
    private int yCell;
    private int scoreIncrease;


    public MyAI()
    {
        super("MyAI");
        LEARN = Main.LEARN_MODE;
        System.out.println("ai LEARN:\t"+LEARN);
        surroundings = new Vector();
        resetAction();
        reset();
    }

    private void initializeLearningTable()
    {
        for(int i = 0; i < learningTable.length; ++i)
        {
            for(int j = 0; j < learningTable[i].length; ++j)
                learningTable[i][j] = new Ratio();
        }


        //These values do not exist in the decision tree. Set to be -1.
        learningTable[0][1] = new Ratio(-1, 1);
        learningTable[1][0] = new Ratio(-1, 1);
        learningTable[1][2] = new Ratio(-1, 1);
    }

    public void resetAction()
    {
        action = new boolean[Environment.numberOfButtons];
        action[Mario.KEY_RIGHT] = true;
        action[Mario.KEY_SPEED] = false;
        action[Mario.KEY_JUMP] = false;
        action[Mario.KEY_LEFT] = false;
        action[Mario.KEY_DOWN] = false;
        trueJumpCounter = 0;

        surroundings.clear();
    }

    public void reset()
    {
        score = 0;
        previousAction = "";
        actionComplete = true;
        currentEnemies = 0;
        currentThreat = -1;
        currentThreatHeight = 0;
        previousMario = -1;
        xCell = -1;
        yCell = -1;
        outputFile = "testAgentTable.txt";


        //Allocate Memory for learningTable.
        learningTable = new Ratio[6][3];

        //Read data from last run episode.
        boolean fileSuccess = new File(outputFile).exists();

        if(fileSuccess)
        {
            try
            {
                BufferedReader input = new BufferedReader(new FileReader(outputFile));
                totalEpisodes = Integer.parseInt(input.readLine());
                learningRate = (double)totalEpisodes / (double)MAX_EPISODES;
                learningRate = learningRate > LEARNING_MIN? learningRate:LEARNING_MIN;
                System.out.println("learningRate: "+learningRate);
                String data = input.readLine();
                int xCount = 0;
                int yCount = 0;
                while(data != null)
                {
                    yCount = 0;
                    //process string here.
                    String[] row = data.split(":");
                    for (int i = 0; i < row.length; i += 2)
                    {
                        int num1 = Integer.parseInt(row[i]);
                        int num2 = Integer.parseInt(row[i+1]);

                        learningTable[xCount][yCount] = new Ratio(num1, num2);
                        yCount++;
                    }
                    xCount++;
                    data = input.readLine();
                }
            }
            catch(Exception e)
            {
                System.out.println("Reading file failed. Creating new table");
                System.out.println(e.getMessage());
                e.printStackTrace();
                fileSuccess = false;
            }
        }

        if(!fileSuccess)
        {
            totalEpisodes = MAX_EPISODES;
            initializeLearningTable();
        }

        learningRate = (double)totalEpisodes / (double)MAX_EPISODES;

        //build the decision tree.
        tree = new DecisionTree();
    }

    //Finds obstacles and pushes them onto surroundings.
    private void findObstacles()
    {
        for(int x1 = 12; x1 < mergedObservation.length; ++x1)
        {
            if(mergedObservation[11][x1] == -10 || mergedObservation[11][x1] == 20)
            {
                //obstacle exists. Find height.
                int y1 = 0;
                int x2 = 0;
                int y2 = 0;
                for(int j = 11; j >= 0 && y1 == 0; --j)
                {
                    if (mergedObservation[j][x1] != -10 && mergedObservation[j][x1] != 20 )
                    {
                        //Assume the same height.
                        y1= j;
                        y2 = y1;
                    }
                }
                for (int j = x1; j < mergedObservation.length && x2 == 0; ++j)
                {
                    if (mergedObservation[y1][j] != -10 || mergedObservation[y1][j] != 20)
                        x2 = j - 1;
                }
                surroundings.addElement(new Obstacle(x1, y1, x2, y2));
            }
        }
    }


    //Finds enemies in the environment and pushes them onto surroundings. Fully functioning.
    private void findEnemies()
    {
        previousEnemies = currentEnemies;
        currentEnemies = 0;
        for(int i = 0; i < enemies.length; ++i)
        {
            for(int j = 11; j < enemies.length; ++j)
            {
                if(enemies[i][j] != 0 && enemies[i][j] != 25)
                {
                    surroundings.addElement(new Enemy(j, i, j+1, i+1, enemies[i][j] / 9));
                    currentEnemies++;
                }
            }
        }
    }

    private void display()
    {
        System.out.print("\t");
        for (int i = 0; i < mergedObservation.length; ++i)
        {
         System.out.print(i+"\t");
        }
        System.out.println();
        
        for (int i = 0; i < mergedObservation.length; ++i)
        {
            System.out.print(i);
            for (int j = 0; j < mergedObservation[i].length; ++j)
            {
                System.out.print("\t"+mergedObservation[i][j]);
            }
            System.out.println();
        }
        System.out.println();

    }

    private void findSurroundings()
    {
        surroundings.clear();
        findObstacles();
        findEnemies();
        Collections.sort(surroundings);
        for(int i = 0; i < surroundings.size(); ++i)
        {
            Element current = (Element)surroundings.get(i);
        }
    }

    /*******
     * Entry point for the AI. This is called by the gaming environment to recieve the AI's instructions.
     * Instructions are returned in the form of a boolean array whose number of elements match the number of
     * buttons. True indicates that the button should be pressed and false indicates tha it should not.
     *
     * @return action[] - array of buttons. true indicates press.
     */
    public boolean[] getAction()
    {
        tree.changeAbleToShoot(isMarioAbleToShoot && marioMode == 2);
        findSurroundings();
        //display();

        /********************************************************************
        * Previous action was completed. Record results in learning table.
        * Traverse the tree and update previousAction with appropriate value.
        ********************************************************************/
        if(actionComplete)
            recordResults();

        //Level has been completed Exit current call after cleanup.
        if (marioStatus < 2)
        {
            endLevelCleanup();
            return action;
        }
        
        //call the appropriate FLO.
        if(previousAction.equals("doJump"))
            doJump();
        else if(previousAction.equals("doRun"))
            doRun();
        else if(previousAction.equals("doRight"))
            doRight();
        else
            doShoot();

        return action;
    }

    private void endLevelCleanup()
    {
        score += marioStatus == 0?-1000:1000; //Handles point distribution on win/loss.
        System.out.println("Score:\t\t"+score);

        //I/O write should occur here. Store Q-Value data in file if learning.
        if(LEARN)
        {
            try
            {
                System.out.println("Saving learningTable to file.\n");
                totalEpisodes--;
                BufferedWriter out = new BufferedWriter(new FileWriter(outputFile));
                out.write(totalEpisodes+"\n");
                for (int i = 0; i < learningTable.length; ++i)
                {
                    for(int j = 0; j < learningTable[i].length - 1; ++j)
                    {
                        out.write(learningTable[i][j].writeRatio()+":");
                    }
                    out.write(learningTable[i][(learningTable[i].length - 1)].writeRatio());
                    out.write("\n");
                }
                out.close();
            }
            catch (Exception e)
            {
                System.out.println("error writing to file.");
            }
        }

        //If we aren't learning, we are evaluating and need to record the scores.
        else
        {
            try
            {
                BufferedWriter out = new BufferedWriter(new FileWriter("score.txt", true));
                String output = ""+score+"\n";
                out.write(output);
                out.close();
            }
            catch (Exception e)
            {
                System.out.println("Recording Results failed.");
            }
        }
    }

    private void recordResults()
    {
        if(xCell > -1 && yCell > -1 && (marioMode >= previousMario && marioStatus != 0))
        {
            learningTable[xCell][yCell].success();
            score += scoreIncrease;
        }
        else
            score -= scoreIncrease;

        previousThreat = currentThreat;
        actionComplete = false;
        Node decision = tree.decide(learningTable, learningRate, surroundings);
        scoreIncrease = decision.getPointValue();
        previousAction = decision.getName();
        currentThreat = surroundings.isEmpty() ? NO_ELEMENT: surroundings.get(0).getClass().getName().equals(OBSTACLE_CLASS) ? OBSTACLE: ENEMY;
        currentThreatHeight = currentThreat == ENEMY ? ((Element)surroundings.get(0)).getHeight(): 0;
        previousMario = marioMode;
        xCell = threatToCell(currentThreat, currentThreatHeight);
        yCell = actionToCell(decision.getName());
        learningTable[xCell][yCell].incrementTries();
    }

    private int actionToCell(String action)
    {
        if(action.equals("doRun"))
            return 0;
        if(action.equals("doJump"))
            return 1;
        if (action != "")
            return 2;
        return -1;
    }

    private int threatToCell(int threat, int height)
    {
        if(threat < 2)
            return threat;
        else
        {
            //return a number based on height of the enemy...
            if(height < 0)
                return 2;
            if(height == 0)
                return 3;
            if(height < 5)
                return 4;
            if (height > 4)
                return 5;
            return -1;
        }
    }

    private void doJump()
    {
        if(currentEnemies < previousEnemies)
        {
//            System.out.println("doJump Complete101!\n");
            actionComplete = true;
            return;
        }

        if(surroundings.size() > 0)
        {
            //Jump until the current obstacle is overcome.
            if(((Element)surroundings.get(0)).getDistance() < 3)
            {
                if(((Element)surroundings.get(0)).getHeight() > -2)
                {
                    if(!(trueJumpCounter > 0 && isMarioOnGround))
                    {
                        action[Mario.KEY_JUMP] = true;
                        action[Mario.KEY_SPEED] = true;
                        trueJumpCounter++;
                    }
                    //Mario's last jump failed and he has hit the ground. Try again.
                    else
                    {
//                        System.out.println("trueJumpCounter reset");
                        resetAction();
                    }
                }
                else
                {
//                    System.out.println("mario greater than threat height reset");
                    resetAction();
                }
            }
            else
            {
                resetAction();
                action[Mario.KEY_SPEED] = true;
            }

        }
        else
        {
//            System.out.println("doJump Complete102!\n");
            resetAction();
            actionComplete = true;
        }
    }

    private void doRun()
    {
        resetAction();
        action[Mario.KEY_SPEED] = true;
//        System.out.println("enemies: "+previousEnemies+"\t"+currentEnemies);
        if(currentEnemies == 0 || currentEnemies < previousEnemies)
        {
            actionComplete = true;
//            System.out.println("doRun Complete!\n");
        }
    }

    private void doRight()
    {
        resetAction();
        actionComplete = true;
//        System.out.println("doRight Complete!\n");
    }

    private void doShoot()
    {
        action[Mario.KEY_SPEED] = !action[Mario.KEY_SPEED];
        action[Mario.KEY_RIGHT] = false;
        if(currentEnemies == 0 || currentEnemies < previousEnemies)
        {
            actionComplete = true;
//            System.out.println("doShoot Complete!\n");
        }
    }
}